/* Copyright © 2023 - 2024 Coremail论客
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import url from '@ohos.url';
import http from '@ohos.net.http';
import { getLogger } from "./log"
import { CMError, ErrorCode } from '../api';

const logger = getLogger('oauth2')

type TokenResult = {
  accessToken: string;
  refreshToken?: string;
  expireTime: number; // accessToken的过期时间，以秒为单位
}

class OAuth2Client {
  clientId: string;
  authEndpoint: string;
  tokenEndpoint: string;
  redirectUri: string;

  constructor(clientId: string, authEndpoint: string, tokenEndpoint: string, redirectUri: string) {
    this.clientId = clientId;
    this.authEndpoint = authEndpoint;
    this.tokenEndpoint = tokenEndpoint;
    this.redirectUri = redirectUri;
  }

  createAuthorizationUrl(scopes: string[]): string {
    const params = new url.URLParams({
      'client_id': this.clientId,
      'response_type': "code",
      'redirect_uri': this.redirectUri,
      // 'response_mode': "query",
      'scope': scopes.join(' '),
    });
    const authUrl = `${this.authEndpoint}?${params.toString()}`;
    logger.info('oauth2 url', authUrl);
    // return `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?${params.toString()}`;
    return authUrl;
  }

  redeemCodeForToken(code: string): Promise<object> {
    // const remoteUrl = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`;
    // const data = `client_id=${this.clientId}&response_type=code&redirect_uri=${encodeURIComponent(this.callback)}&response_mode=query&scope=mail.read%20mail.readwrite%20mail.send`;
    const params = new url.URLParams({
      'client_id': this.clientId,
      'grant_type': 'authorization_code',
      // 'scope': scope,
      'redirect_uri': this.redirectUri,
      // 'client_secret': this.clientSecret,
      'code': code,
    });
    return this.requestToken(params);
  }

  refreshToken(token: string): Promise<object> {
    const params = new url.URLParams({
      'client_id': this.clientId,
      'grant_type': 'refresh_token',
      'refresh_token': token,
      // 'redirect_uri': this.redirectUri,
      // 'client_secret': this.clientSecret,
    });
    return this.requestToken(params);
  }

  async requestToken(params: url.URLParams): Promise<object> {
    const query = params.toString();
    const request = http.createHttp();
    let response: http.HttpResponse;
    try {
      response = await request.request(this.tokenEndpoint, {
        method: http.RequestMethod.POST,
        header: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Accept': 'application/json',
          'User-Agent': 'chord-mail'
        },
        extraData: params.toString()
      })
    } catch(e) {
      logger.error("error", e);
      throw new CMError(`oauth2 failed ${this.tokenEndpoint} ${query}`, ErrorCode.OAUTH2_REQUEST_TOKEN_FAILED)
    }
    logger.info("response", response.resultType, response.responseCode, response.result);
    if (response.responseCode != http.ResponseCode.OK) {
      throw new CMError(`oauth2 http error ${this.tokenEndpoint} ${query} ${response.responseCode}`,
        ErrorCode.OAUTH2_REQUEST_TOKEN_FAILED,
        response.result
      );
    } else {
      let obj: object;
      if (response.resultType == http.HttpDataType.STRING) {
        obj = JSON.parse(response.result as string);
      } else if (response.resultType == http.HttpDataType.OBJECT) {
        obj = response.result as object;
      } else {
        throw new CMError(`oauth2 bad ${this.tokenEndpoint} ${query}`,
          ErrorCode.OAUTH2_REQUEST_TOKEN_FAILED,
          response.result
        );
      }
      if (obj['error'] || !obj['access_token']) {
        throw new CMError(`oauth2 error ${this.tokenEndpoint} ${query}`,
          ErrorCode.OAUTH2_REQUEST_TOKEN_FAILED,
          obj
        );
      }
      return obj;
    }
  }
}


export type CheckResultUrl = (url: string) => boolean
export type PromptForUserAuthorize = (url: string, checkResult: CheckResultUrl) => void

export class OAuth2Outlook {
  oauthClient: OAuth2Client;
  accessToken?: string;
  refreshToken?: string;
  idToken?: string;
  expire?: number;
  redirectUri: string = 'https://login.microsoftonline.com/common/oauth2/nativeclient'
  scopes: string[] = [
    'https://outlook.office.com/IMAP.AccessAsUser.All',
    'https://outlook.office.com/SMTP.Send',
    'offline_access',
    "openid"
  ];

  constructor(clientId: string, tenant: string = 'common') {
    this.oauthClient = new OAuth2Client(clientId,
      `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
      `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
      this.redirectUri
    )
  }

  async refresh(refreshToken?: string): Promise<string> {
    if (refreshToken) {
      this.refreshToken = refreshToken;
    }
    if (!this.refreshToken) {
      throw new CMError('no refresh token');
    }
    const obj = await this.oauthClient.refreshToken(this.refreshToken);
    this.updateTokens(obj);
    return this.accessToken!;
  }

  getRefreshToken(): string | undefined {
    return this.refreshToken;
  }

  updateTokens(obj: object) {
    this.accessToken = obj['access_token'] as string;
    this.refreshToken = obj['refresh_token'] as string;
    this.idToken = obj['id_token'] as string;
    this.expire = obj['expire_in']
  }

  async requestAccessToken(prompt: PromptForUserAuthorize): Promise<string> {
    if (this.accessToken) {
      return this.accessToken;
    }
    const authUrl = this.oauthClient.createAuthorizationUrl(this.scopes);
    const code = await new Promise<string>((resolve, reject) => {
      prompt(authUrl, (redirectUri: string) => {
        if (redirectUri.startsWith(this.redirectUri)) {
          const r = url.URL.parseURL(redirectUri);
          const params = new url.URLParams(r.search);
          const code = params.get('code')
          const error = params.get('error')
          if (code) {
            resolve(code);
          } else if (error) {
            reject(error);
          }
          return true;
        }
        return false;
      });
    });
    const obj = await this.oauthClient.redeemCodeForToken(code);
    this.updateTokens(obj);
    return this.accessToken!;
  }
}